package com.gitee.wsl.data.input.strategy

import com.gitee.wsl.coroutines.Queued
import com.gitee.wsl.data.input.InputStrategy
import com.gitee.wsl.data.input.InputStrategyScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch

/**
 * Unlike LIFO and FIFO strategies, Parallel gives no guarantee that inputs will be processed in any given order or
 * that only 1 Input will be processing at a time. However, it allows you to have inputs that both: start running
 * immediately without getting blocked, while also guaranteeing that long-running inputs will not get cancelled.
 * Parallel is suitable for processing complex UIs that have many Input sources and many long-running tasks, but should
 * not be the first choice to UI ViewModels. Prefer a LIFO strategy with an input filter for UIs, and only use this in
 * specific scenarios where that becomes difficult to manage. Also consider moving long-running work to a side-job
 * instead of dropping the entire ViewModel into parallel-processing mode, if that work doesn't need to perform state
 * updates.
 *
 * However, since inputs are being processed in parallel, it allows the possibility of race conditions if multiple
 * inputs update the ViewModel state, but get interleaved with each other. To prevent this and protect the purity of the
 * "state machine" where everything is deterministic with respect to the ordering of inputs, this strategy adds a
 * restriction that each Input may only access or update the ViewModel state at most 1 time. Individual State accesses
 * and updates are atomic and so are protected against race conditions, but performing multiple updates would require
 * some kind of synchronization to be safe, which goes against the state machine philosophy.
 *
 * Because multiple inputs may be processed at once, if an input is cancelled there is no meaningful way to know what
 * state should be rolled-back to. Cancelled inputs may leave the ViewModel in a bad state.
 */
 class ParallelInputStrategy<Inputs : Any, Events : Any, State : Any> private constructor(
) : ChannelInputStrategy<Inputs, Events, State>(
    capacity = Channel.BUFFERED,
    onBufferOverflow = BufferOverflow.SUSPEND,
    filter = null,
) {

    override suspend fun InputStrategyScope<Inputs, Events, State>.processInputs(
        filteredQueue: Flow<Queued<Inputs, Events, State>>,
    ) {
        coroutineScope {
            val viewModelScope = this

            filteredQueue
                .collect { queued ->
                    viewModelScope.launch {
                        acceptQueued(queued, Guardian()) {
                            // do nothing, we cannot rollback in Parallel without creating race conditions in other jobs
                        }
                    }
                }
        }
    }

     class Guardian : DefaultGuardian() {
        private fun performStateAccessCheck() {
            check(!stateAccessed) {
                "ParallelInputStrategy requires that inputs only access or update the state at most once as a " +
                        "safeguard against race conditions."
            }
        }

        override fun checkStateAccess() {
            performStateAccessCheck()
            super.checkStateAccess()
        }

        override fun checkStateUpdate() {
            performStateAccessCheck()
            super.checkStateUpdate()
        }
    }

     companion object {
         operator fun invoke(): ParallelInputStrategy<Any, Any, Any> {
            return ParallelInputStrategy()
        }

         fun <Inputs : Any, Events : Any, State : Any> typed(): ParallelInputStrategy<Inputs, Events, State> {
            return ParallelInputStrategy()
        }
    }
}

/**
 * A default [InputStrategy.Guardian] that may be used by a custom [InputStrategy] for ensuring Inputs are handled the
 * same way as the built-in strategies. It can be overridden to make slight tweaks to its behavior, but larger tweaks
 * should be done by creating a fully-custom Guardian implementation.
 *
 * This guardian enforces the following rules:
 *
 * - Nothing gets changed after the InputHandler returns from processing an Input (such as coroutine launched in
 *      parallel attempting to use a reference to the original InputHandlerScope).
 * - Starting a SideJob must be the last statement of the InputHandler. You can start multiple side-jobs from a single
 *      Input, but all of them must be the last statements of the InputHandler. This helps prevent issues from the fact
 *      that SideJobs are running as a lambda, but do not get started immediately when `sideJob()` is called.
 * - There should be _something_ done during the processing of an event. This helps prevent Input types from
 *      accidentally getting ignored, or from using an Input incorrectly (updating variables anywhere other than the VM
 *      State, launching coroutines rather than using SideJobs, etc.)
 */
open class DefaultGuardian : InputStrategy.Guardian {

    protected var stateAccessed: Boolean = false
    protected var sideJobsPosted: Boolean = false
    protected var usedProperly: Boolean = false
    protected var closed: Boolean = false

    override fun checkStateAccess() {
        checkNotClosed()
        checkNoSideJobs()
        stateAccessed = true
        usedProperly = true
    }

    override fun checkStateUpdate() {
        checkNotClosed()
        checkNoSideJobs()
        stateAccessed = true
        usedProperly = true
    }

    override fun checkPostEvent() {
        checkNotClosed()
        checkNoSideJobs()
        usedProperly = true
    }

    override fun checkNoOp() {
        checkNotClosed()
        checkNoSideJobs()
        usedProperly = true
    }

    override fun checkSideJob() {
        checkNotClosed()
        sideJobsPosted = true
        usedProperly = true
    }

    override fun close() {
        checkNotClosed()
        checkUsedProperly()
        closed = true
    }

// Inner checks
// ---------------------------------------------------------------------------------------------------------------------

    private fun checkNotClosed() {
        check(!closed) { "This InputHandlerScope has already been closed" }
    }

    private fun checkNoSideJobs() {
        check(!sideJobsPosted) {
            "Side-Jobs must be the last statements of the InputHandler"
        }
    }

    private fun checkUsedProperly() {
        check(usedProperly) {
            "Input was not handled properly. To ensure you're following the MVI model properly, make sure any " +
                    "side-jobs are executed in a `sideJob { }` block."
        }
    }
}